﻿using System;
using System.Collections.Generic;
using System.Text;

namespace dotHTML {
    /// <summary>
    /// Element is the class from which all HTML elements derive.
    /// </summary>
    public abstract class Element {
        /// <summary>
        /// Determines the text direction for the "dir" attribute.
        /// </summary>
        public enum TextDirection {
            /// <summary>
            /// Text direction is left-to-right.
            /// </summary>
            LTR,
            /// <summary>
            /// Text direction is right-to-left.
            /// </summary>
            RTL,
            /// <summary>
            /// Text direction is determined by the user agent.
            /// </summary>
            Auto
        }

        /// <summary>
        /// Determines what happens when an item is dragged and dropped
        /// onto this one, for the "dropzone" attribute.
        /// </summary>
        public enum DropZoneAction {
            /// <summary>
            /// Dragging and dropping will create a copy of the item.
            /// </summary>
            Copy,
            /// <summary>
            /// Dragging and dropping will move the item.
            /// </summary>
            Move,
            /// <summary>
            /// Dragging and dropping will link the item.
            /// </summary>
            Link
        }

        // HTML attributes
        private ICollection<char> _accesskey;
        private IList<string> _class;
        private bool? _contentEditable;
        private IDictionary<string, string> _dataset;
        private TextDirection? _dir;
        private bool? _draggable;
        private DropZoneAction? _dropzone;
        private IDictionary<string, string> _events;
        private bool _hidden;
        private string _id;
        private string _lang;
        private bool? _spellcheck;
        private string _style;
        private int? _tabIndex;
        private string _title;
        private IList<Element> _children;

        /// <summary>
        /// Constructor for an Element that instantiates all required data
        /// structures. Called only during construction of subclassed elements.
        /// </summary>
        protected Element() {
            _accesskey = new LinkedList<char>();
            _class = new List<string>();
            _dataset = new Dictionary<string, string>();
            _events = new Dictionary<string, string>();
            _children = new List<Element>();
        }

        /// <summary>
        /// Constructs the element with the specified text contained within it.
        /// </summary>
        /// <param name="text">The text to contain within this element.</param>
        /// <remarks>Has no effect if the target element is a Standalone
        /// element.</remarks>
        protected Element(string text) {
            this.Children.Add(new TextNode(text));
        }

        /// <summary>
        /// Gets a collection of all access keys associated with this element.
        /// </summary>
        public ICollection<char> AccessKey {
            get { return _accesskey; }
        }

        /// <summary>
        /// Gets a list of the children of this element.
        /// </summary>
        public IList<Element> Children {
            get { return _children; }
        }

        /// <summary>
        /// Gets a list of all classes associated with this element. It is up
        /// to the user to ensure that no duplicates are placed.
        /// </summary>
        public IList<string> Class {
            get { return _class; }
        }

        /// <summary>
        /// Determines whether or not this element is user-modifiable. Set to
        /// null to omit.
        /// </summary>
        public bool? ContentEditable {
            get { return _contentEditable; }
            set { _contentEditable = value; }
        }

        /// <summary>
        /// Returns a dictionary of all dataset members of this element.
        /// </summary>
        public IDictionary<string, string> DataSet {
            get { return _dataset; }
        }

        /// <summary>
        /// Determines the text direction of this element. Set to null to omit.
        /// </summary>
        public TextDirection? Dir {
            get { return _dir; }
            set { _dir = value; }
        }

        /// <summary>
        /// Determines whether this element is draggable. Set to null to omit.
        /// </summary>
        public bool? Draggable {
            get { return _draggable; }
            set { _draggable = value; }
        }

        /// <summary>
        /// Determines what happens when another element is dropped onto this
        /// element. Set to null to omit.
        /// </summary>
        public DropZoneAction? DropZone {
            get { return _dropzone; }
            set { _dropzone = value; }
        }

        /// <summary>
        /// Returns a dictionary of all DOM events of this element.
        /// </summary>
        public IDictionary<string, string> Events {
            get { return _events; }
        }

        /// <summary>
        /// Determines whether this element is hidden. Set to null to omit.
        /// </summary>
        public bool Hidden {
            get { return _hidden; }
            set { _hidden = value; }
        }

        /// <summary>
        /// Gets or sets the ID of this element. Set to null to omit.
        /// </summary>
        public string ID {
            get { return _id; }
            set { _id = value; }
        }

        /// <summary>
        /// Gets or sets the language of this element. Set to null to omit.
        /// </summary>
        public string Lang {
            get { return _lang; }
            set { _lang = value; }
        }

        /// <summary>
        /// Determines whether or not to spellcheck this element. Set to null
        /// to omit.
        /// </summary>
        public bool? Spellcheck {
            get { return _spellcheck; }
            set { _spellcheck = value; }
        }

        /// <summary>
        /// Gets or sets the CSS style associated with this element. Set to
        /// null to omit.
        /// </summary>
        public string Style {
            get { return _style; }
            set { _style = value; }
        }

        /// <summary>
        /// Determines the tab index of this element. Set to null to omit.
        /// </summary>
        public int? TabIndex {
            get { return _tabIndex; }
            set { _tabIndex = value; }
        }

        /// <summary>
        /// Determines the title of this element. Set to null to omit.
        /// </summary>
        public string Title {
            get { return _title; }
            set { _title = value; }
        }

        /// <summary>
        /// Gets a formatted string of all the base (non-specific) attributes 
        /// of this element.
        /// </summary>
        /// <param name="sb">The StringBuilder to write this element's
        /// attribute data to.</param>
        private void WriteBaseAttributes(StringBuilder sb) {
            AttributeWriter.Write(sb, _accesskey, "accesskey", ' ');
            AttributeWriter.Write(sb, _class, "class", ' ');
            AttributeWriter.Write(sb, _contentEditable, "contenteditable");
            AttributeWriter.Write(sb, _dataset, "data-");
            AttributeWriter.Write(sb, _dir, "dir", true);
            AttributeWriter.Write(sb, _draggable, "draggable");
            AttributeWriter.Write(sb, _dropzone, "dropzone", true);
            AttributeWriter.Write(sb, _events, null);
            AttributeWriter.Write(sb, _hidden, "hidden");
            AttributeWriter.Write(sb, _id, "id");
            AttributeWriter.Write(sb, _lang, "lang");
            AttributeWriter.Write(sb, _spellcheck, "spellcheck");
            AttributeWriter.Write(sb, _style, "style");
            AttributeWriter.Write(sb, _tabIndex, "tabindex");
            AttributeWriter.Write(sb, _title, "title");
        }

        /// <summary>
        /// Gets the raw data corresponding to this element.
        /// </summary>
        /// <returns>The raw data corresponding to this element.</returns>
        public override String ToString() {
            /* if this is a text node, simply return the text. text nodes
             * cannot have attributes or children, and have no start or end
             * tags. */
            if (this is TextNode) {
                return ((TextNode)this).Text;
            }

            // the name of the element being written, e.g. "span"
            string elemName = this.GetType().Name.ToLower();

            StringBuilder sb = new StringBuilder();
            sb.Append('<');

            sb.Append(elemName);

            WriteBaseAttributes(sb);
            WriteLocalAttributes(sb);

            // determine if this is a standalone element and act accordingly
            if (Attribute.GetCustomAttribute(this.GetType(),
                typeof(Standalone)) == null) { // write children and end tag
                sb.Append('>');

                WriteChildren(sb);

                sb.Append("</");
                sb.Append(elemName);
                sb.Append('>');
            } else { // write immediate end tag
                sb.Append("/>");
            }

            return sb.ToString();
        }

        /// <summary>
        /// Gets a formatted string of all local attributes of this element
        /// (not including the base attributes).
        /// </summary>
        /// <param name="sb">The StringBuilder to write this element's
        /// attribute data to.</param>
        protected virtual void WriteLocalAttributes(StringBuilder sb) { }

        /// <summary>
        /// Gets a formatted string of all child elements of this element.
        /// </summary>
        /// <param name="sb">The StringBuilder to write this element's child
        /// data to.</param>
        private void WriteChildren(StringBuilder sb) {
            foreach (Element elem in _children) {
                sb.Append(elem.ToString());
            }
        }
    }
}
